xxxxxxxxxx# Исследование рынка игрДанное ставит перед собой цели:* Исследовать закономерности среди наиболее успешных игр* Сравнивнить продажи среди разных платформ и жанров* Определить наиболее сильные признаки успешных видеоигрЭто поможет сделать ставку на потенциально популярных играх и спланировать рекламные компании. Исследование базируется на исторических данных из открытых источников.**Структура данных:**Информация о продажах видеоигр хранится в файле `games_data.csv`:* `Name` — название видеоигры* `Platform` — платформа* `Year_of_Release` — год выпуска* `Genre` — жанр* `NA_sales` — продажи в Северной Америке (миллионы проданных копий)* `EU_sales` — продажи в Европе (миллионы проданных копий)* `JP_sales` — продажи в Японии (миллионы проданных копий)* `Other_sales` — продажи в других странах (миллионы проданных копий)* `Critic_Score` — оценка критиков (максимум 100)* `User_Score` — оценка пользователей (максимум 10)* `Rating` — возрастной рейтинг от организации ESRB **План:**<div class="toc"> <ul class="toc-item"> <li><span><a href="#Setup" data-toc-modified-id="Setup-2">Setup</a></span></li> <li><span><a href="#Предобработка-данных" data-toc-modified-id="Предобработка-данных-3">Предобработка данных</a></span></li> <li><span><a href="#Исследовательский-анализ-данных" data-toc-modified-id="Исследовательский-анализ-данных-4">Исследовательский анализ данных</a></span></li> <li> <span><a href="#Тестирование-гипотез" data-toc-modified-id="Тестирование-гипотез-5">Тестирование гипотез</a></span> <ul class="toc-item"> <li><span><a href="#Средние-пользовательские-рейтинги-платформ-Xbox-One-и-PC-одинаковые" data-toc-modified-id="Средние-пользовательские-рейтинги-платформ-Xbox-One-и-PC-одинаковые-5.1">Средние пользовательские рейтинги платформ Xbox One и PC одинаковые</a></span></li> <li><span><a href="#Средние-пользовательские-рейтинги-жанров-Action-и-Sports-разные." data-toc-modified-id="Средние-пользовательские-рейтинги-жанров-Action-и-Sports-разные.-5.2">Средние пользовательские рейтинги жанров Action и Sports разные.</a></span></li> </ul> </li> <li><span><a href="#Важность-признаков" data-toc-modified-id="Важность-признаков-6">Важность признаков</a></span></li> <li><span><a href="#Итог" data-toc-modified-id="Итог-7">Итог</a></span></li> </ul></div>Данное ставит перед собой цели:
Это поможет сделать ставку на потенциально популярных играх и спланировать рекламные компании.
Исследование базируется на исторических данных из открытых источников.
Структура данных:
Информация о продажах видеоигр хранится в файле games_data.csv:
Name — название видеоигрыPlatform — платформаYear_of_Release — год выпускаGenre — жанрNA_sales — продажи в Северной Америке (миллионы проданных копий)EU_sales — продажи в Европе (миллионы проданных копий)JP_sales — продажи в Японии (миллионы проданных копий)Other_sales — продажи в других странах (миллионы проданных копий)Critic_Score — оценка критиков (максимум 100)User_Score — оценка пользователей (максимум 10)Rating — возрастной рейтинг от организации ESRB План:
xxxxxxxxxximport numpy as npimport pandas as pdfrom scipy import statsfrom scipy.optimize import curve_fitfrom matplotlib_inline.backend_inline import set_matplotlib_formatsimport plotly.io as pioimport plotly.express as pxfrom plotly.subplots import make_subplotsimport plotly.figure_factory as fffrom sklearn.model_selection import train_test_split, ShuffleSplitfrom sklearn.metrics import r2_score, mean_absolute_errorfrom sklearn.inspection import permutation_importancefrom catboost import Pool, CatBoostRegressor, cvimport shapfrom ipywidgets import interact, interact_manualimport ipywidgets as widgetsxxxxxxxxxxnp.random.seed(42)pd.set_option('display.float_format', '{:.2f}'.format)set_matplotlib_formats('svg')pio.templates.default = 'plotly_white'pio.templates['plotly_white']['layout']['font'] = {'color': '#2a3f5f', 'size': 14}shap.initjs()xxxxxxxxxxdata = pd.read_csv('https://github.com/rusmux/yandex-games/blob/main/games_data.csv?raw=true')dataxxxxxxxxxxВ столбце `User_Score` встречается аббревиатура tbd - to be determined, будем считать ее как пропущенное значение.В столбце User_Score встречается аббревиатура tbd - to be determined, будем считать ее как пропущенное значение.
xxxxxxxxxxdata.columns = data.columns.str.lower()data = data.replace('tbd', np.nan).apply(pd.to_numeric, downcast='float', errors='ignore')data['decade_of_release'] = pd.cut(data['year_of_release'], range(1979, 2029, 10), labels=['1980s', '1990s', '2000s', '2010s'])data['total_sales'] = data['na_sales'] + data['eu_sales'] + data['jp_sales'] + data['other_sales']xxxxxxxxxxdata.info(memory_usage='deep')xxxxxxxxxxdata.isna().agg(['sum', 'mean']).T.rename_axis('missing values', axis=1)xxxxxxxxxxПосмотрим, откуда берутся пропуски в оценках критиков и игроков. Возможно, на раннем этапе развития видеоигр мало кто оставлял отзывы и оценки.Посмотрим, откуда берутся пропуски в оценках критиков и игроков. Возможно, на раннем этапе развития видеоигр мало кто оставлял отзывы и оценки.
xxxxxxxxxxnan_mean = lambda x: x.isna().mean()nan_by_decade = data.pivot_table(index='decade_of_release', values=['user_score', 'critic_score', 'rating'], aggfunc=[nan_mean]).rename({'<lambda>': 'mean_nan'}, axis=1, level=0)nan_by_decade['total_games'] = data['decade_of_release'].value_counts()nan_by_decadexxxxxxxxxxВидим, что в самом начале эры видеоигр почти не было оценок критиков и игроков, но и игр тогда было мало. Большая часть пропусков идет с последних двух десятилетий - доля игр без критики уменьшилась, но выросло общее количество игр. Не будем заполнять пропуски, так как их слишком много.Видим, что в самом начале эры видеоигр почти не было оценок критиков и игроков, но и игр тогда было мало. Большая часть пропусков идет с последних двух десятилетий - доля игр без критики уменьшилась, но выросло общее количество игр. Не будем заполнять пропуски, так как их слишком много.
xxxxxxxxxxПосмотрим на наличие дубликатов.Посмотрим на наличие дубликатов.
xxxxxxxxxxdata.duplicated().sum()xxxxxxxxxxПроверим, есть ли неявные дубликаты в именах.Проверим, есть ли неявные дубликаты в именах.
xxxxxxxxxxassert data['platform'].str.lower().nunique() == data['platform'].nunique()assert data['name'].str.lower().nunique() == data['name'].nunique()assert data['genre'].str.lower().nunique() == data['genre'].nunique()assert data['rating'].str.lower().nunique() == data['rating'].nunique()print('No implicit duplicates')xxxxxxxxxxИли неявные дубликаты по имени и платформе.Или неявные дубликаты по имени и платформе.
xxxxxxxxxxdata.duplicated(subset=['name', 'platform']).sum()xxxxxxxxxxДействительно что-то есть.Действительно что-то есть.
xxxxxxxxxxdata[data.duplicated(subset=['name', 'platform'])]xxxxxxxxxxВероятнее всего, это какие-нибудь ремейки.Вероятнее всего, это какие-нибудь ремейки.
xxxxxxxxxxdata.query("name == 'Need for Speed: Most Wanted' and platform in ['X360', 'PC']")xxxxxxxxxxВероятнее всего, так и есть, это ремейк 2012 года. Я удалю ремейки, так как они не являются новыми играми и их успешность смещена.Вероятнее всего, так и есть, это ремейк 2012 года. Я удалю ремейки, так как они не являются новыми играми и их успешность смещена.
xxxxxxxxxxdata.drop([1190, 11715], inplace=True)xxxxxxxxxxdata.query("name == 'Madden NFL 13' and platform == 'PS3'")xxxxxxxxxxdata.query("name == 'Sonic the Hedgehog' and platform == 'PS3'")xxxxxxxxxxdata[data['name'].isna()]xxxxxxxxxxОднако с Madden NFL 13, Sonic the Hedgehog и с одной безымянной игрой дело обстоит иначе. Скорее всего, это записи одной и той же игры. Объединим строки с первыми двумя играми, а безымянную игру удалим.Однако с Madden NFL 13, Sonic the Hedgehog и с одной безымянной игрой дело обстоит иначе. Скорее всего, это записи одной и той же игры. Объединим строки с первыми двумя играми, а безымянную игру удалим.
xxxxxxxxxxdata.loc[604, 'eu_sales'] += data.loc[16230, 'eu_sales']data.loc[1745, 'eu_sales'] += data.loc[4127, 'eu_sales']data.drop([4127, 16230, 659, 14244], inplace=True)xxxxxxxxxxprint('Value counts by column')interact(lambda column: data[column].value_counts(normalize=True, dropna=False), column=['platform', 'genre', 'rating', 'decade_of_release']);xxxxxxxxxxprint('Unique games:', data['name'].nunique())data['name'].value_counts()[:20]xxxxxxxxxxАбсолютным рекордсменом по количеству поддерживаемых платформ является Need for Speed: Most Wanted. Особой популярностью отделяются игры серии LEGO. Добавим столбец с количеством платформ у каждой игры. Однако не факт, что этот параметр будет доступен в момент прогнозирования продаж новых видеоигр.Абсолютным рекордсменом по количеству поддерживаемых платформ является Need for Speed: Most Wanted. Особой популярностью отделяются игры серии LEGO. Добавим столбец с количеством платформ у каждой игры. Однако не факт, что этот параметр будет доступен в момент прогнозирования продаж новых видеоигр.
xxxxxxxxxxgame_counts = data['name'].value_counts(dropna=False)data['n_platforms'] = game_counts[data['name']].valuesxxxxxxxxxxdata['rating'].fillna('No rating', inplace=True)xxxxxxxxxxВ датасете отсутствует информация о цене игры, хотя я думаю, она немало влияет на продажи игры. Несмотря на то, что для прогнозирования продаж на следующие года не все информация будет актуальной, некоторые зависимости вполне могут не зависеть от времени и наблюдаться всегда.В датасете отсутствует информация о цене игры, хотя я думаю, она немало влияет на продажи игры. Несмотря на то, что для прогнозирования продаж на следующие года не все информация будет актуальной, некоторые зависимости вполне могут не зависеть от времени и наблюдаться всегда.
xxxxxxxxxxdata.describe(datetime_is_numeric=True)xxxxxxxxxxУдалим выбивающиеся значения и переведем продажи из миллионов в тысячи.Удалим выбивающиеся значения и переведем продажи из миллионов в тысячи.
xxxxxxxxxxoutliers_idxs = data.query("na_sales > 2 or eu_sales > 1 or jp_sales > 1 or other_sales > 1").indexprint(f"Data deleted: < {100*len(outliers_idxs)/len(data):.2f}% ({len(outliers_idxs)}/{len(data)})")data.drop(outliers_idxs, inplace=True)data[['na_sales', 'eu_sales', 'jp_sales', 'other_sales', 'total_sales']] *= 1000xxxxxxxxxxdef cat_feature_histograms(data, years, platforms): data = data.query('@years[1] >= year_of_release >= @years[0]') top_platforms = data.pivot_table(index='platform', values='total_sales', aggfunc='sum') top_platforms = top_platforms.sort_values(by='total_sales', ascending=False)[:platforms].index top_platforms_data = data.query("platform in @top_platforms") fig = px.histogram(top_platforms_data, y='platform') buttons = [] for column in ['platform', 'genre', 'rating', 'decade_of_release']: button=dict( args=[{'y': [top_platforms_data[column] if column == 'platform' else data[column]]}, {'yaxis': {'categoryorder': 'total ascending', 'title': column}}], label=column, method='update', ) buttons.append(button) menu = dict( buttons=buttons, direction='down', x=-0.13, ) fig.update_layout( updatemenus=[menu], yaxis_categoryorder='total ascending', title='Feature histograms', ) return figxxxxxxxxxx# platforms slider is only for "platform" optionyears_slider = widgets.IntRangeSlider(value=[2013, 2016], min=1980, max=2016, continuous_update=False)platforms_slider = widgets.IntSlider(value=10, min=1, max=31, continuous_update=False)interact(cat_feature_histograms, data=widgets.fixed(data), years=years_slider, platforms=platforms_slider);xxxxxxxxxxСамыми популярными платформами за все время являются DS, PS2 и Wii. В последние 3 года лидируют платформы серии PlayStation, 3DS и XOne. Общие пропорции жанров со временем поменялась не сильно. Больше всего игр экшн, спортивных и ролевых игр, шутеров и приключений. Больше всего игр без рейтинга. Потом по количеству за все время идут рейтинги E и T. В последние 3 года их обходит рейтинг M.Игр с рейтингами AO, RP, K-A и EC - единицы. Не будем их учитывать. Также есть очень редкие платформы, мы уберем платформы с количеством игр меньше 100. Однако платформы с малым количеством игр могут быть просто новыми, и на них еще не успели выпустить игры. Тоже самое может быть с рейтингами. Проверим это.Самыми популярными платформами за все время являются DS, PS2 и Wii. В последние 3 года лидируют платформы серии PlayStation, 3DS и XOne.
Общие пропорции жанров со временем поменялась не сильно. Больше всего игр экшн, спортивных и ролевых игр, шутеров и приключений.
Больше всего игр без рейтинга. Потом по количеству за все время идут рейтинги E и T. В последние 3 года их обходит рейтинг M.
Игр с рейтингами AO, RP, K-A и EC - единицы. Не будем их учитывать. Также есть очень редкие платформы, мы уберем платформы с количеством игр меньше 100. Однако платформы с малым количеством игр могут быть просто новыми, и на них еще не успели выпустить игры. Тоже самое может быть с рейтингами. Проверим это.
xxxxxxxxxxratings_to_drop = ['EC', 'K-A', 'RP', 'AO']platforms_to_drop = data['platform'].value_counts()[data['platform'].value_counts() < 100].indexidxs_to_drop = data.query("rating in @ratings_to_drop or platform in @platforms_to_drop").indexxxxxxxxxxxdata.query('platform in @platforms_to_drop').pivot_table(index='platform', values='year_of_release', aggfunc='max').rename(lambda x: 'max_' + x, axis=1)xxxxxxxxxxdata.query('rating in @ratings_to_drop').pivot_table(index='rating', values='year_of_release', aggfunc='max').rename(lambda x: 'max_' + x, axis=1)xxxxxxxxxxКак видим, последняя такая игра вышла в 2011 году, так что можно их удалять.Как видим, последняя такая игра вышла в 2011 году, так что можно их удалять.
xxxxxxxxxxprint(f"Data deleted: < {100*len(idxs_to_drop)/len(data):.2f}% ({len(idxs_to_drop)}/{len(data)})")data = data.drop(idxs_to_drop).reset_index(drop=True)xxxxxxxxxxdef num_feature_histograms(data, years): data = data.query('@years[1] >= year_of_release >= @years[0]') fig = px.histogram(data, x='total_sales', marginal='box') columns_to_show = ['total_sales', 'na_sales', 'eu_sales', 'jp_sales', 'other_sales', 'critic_score', 'user_score', 'year_of_release', 'n_platforms'] buttons = [] for column in columns_to_show: button=dict( args=[{'x': [data[column]]}, {'xaxis': {'rangeslider': {'visible': True}, 'title': column}}], label=column, method='update', ) buttons.append(button) menu = dict( buttons=buttons, direction='down', ) fig.update_layout( updatemenus=[menu], xaxis_rangeslider_visible=True, title='Feature histograms', ) return figxxxxxxxxxxyears_slider = widgets.IntRangeSlider(value=[2013, 2016], min=1980, max=2016, continuous_update=False)interact(num_feature_histograms, data=widgets.fixed(data), years=years_slider);xxxxxxxxxxБольше всего игр вышло в 2008 и 2009 года, а потом произошел резкий спад. В 2015 вышло почти в 2.5 раза меньше игр, чем в 2008. Продажи имеют примерно экспоненциальные распределения. Распределения оценок критиков и игроков похожи на нормальные, но имеют отрицательную асимметрию.Больше всего игр вышло в 2008 и 2009 года, а потом произошел резкий спад. В 2015 вышло почти в 2.5 раза меньше игр, чем в 2008. Продажи имеют примерно экспоненциальные распределения. Распределения оценок критиков и игроков похожи на нормальные, но имеют отрицательную асимметрию.
xxxxxxxxxxdef corr(data, years): data = data.query('@years[1] >= year_of_release >= @years[0]') return px.imshow(data.corr(), text_auto='.2f', title='Correlation matrix')xxxxxxxxxxyears_slider = widgets.IntRangeSlider(value=[2013, 2016], min=1980, max=2016, continuous_update=False)interact(corr, data=widgets.fixed(data), years=years_slider);xxxxxxxxxxПродажи относительно сильно скоррелированы между собой, за исключением продаж в Японии. Возможно, у пользователей в Японии предпочтения сильно отличаются от всего мира. Больше всего общие продажи скоррелированы с продажами в Северной Америке, вероятно, там наибольший рынок сбыта. С остальными признаками однако продажи коррелируют не сильно. Больше всего они коррелируют с оценкой критиков. Оценки критиков и пользователей тоже достаточно сильно скоррелированы. Последние 10 лет наблюдается повышенная корреляция продаж с количеством поддерживаемых платформ.Продажи относительно сильно скоррелированы между собой, за исключением продаж в Японии. Возможно, у пользователей в Японии предпочтения сильно отличаются от всего мира. Больше всего общие продажи скоррелированы с продажами в Северной Америке, вероятно, там наибольший рынок сбыта. С остальными признаками однако продажи коррелируют не сильно. Больше всего они коррелируют с оценкой критиков. Оценки критиков и пользователей тоже достаточно сильно скоррелированы. Последние 10 лет наблюдается повышенная корреляция продаж с количеством поддерживаемых платформ.
xxxxxxxxxxdef sales_by_region(data, years): region_sales = (data .query('@years[0] <= year_of_release <= @years[1]') .melt(value_vars=['na_sales', 'eu_sales', 'jp_sales', 'other_sales'], var_name='region', value_name='sales') .replace({'na_sales': 'North America', 'eu_sales': 'European Union', 'jp_sales': 'Japan', 'other_sales': 'Others'})) fig = px.pie(region_sales, names='region', values='sales') fig.update_traces(textposition='inside', textinfo='label+percent') fig.update_layout(title='Total sales by region', showlegend=False) return figxxxxxxxxxxyears_slider = widgets.IntRangeSlider(value=[2013, 2016], min=1980, max=2016, continuous_update=False)interact(sales_by_region, data=widgets.fixed(data), years=years_slider);xxxxxxxxxxДействительно, в Северной Америке самые большие продажи.Действительно, в Северной Америке самые большие продажи.
xxxxxxxxxxtop_platforms = data.pivot_table(index='platform', values='total_sales', aggfunc='sum')top_platforms = list(top_platforms.sort_values(by='total_sales', ascending=False).index)xxxxxxxxxxdef feature_scatterplots(data, years, platform=None): data = data.query('@years[1] >= year_of_release >= @years[0]') if platform: data = data.query('platform == @platform') fig = px.scatter(data, x='user_score', y='total_sales') x_columns = ['user_score', 'critic_score', 'year_of_release', 'n_platforms', 'total_sales', 'na_sales', 'eu_sales', 'jp_sales', 'other_sales'] y_columns = ['total_sales', 'na_sales', 'eu_sales', 'jp_sales', 'other_sales', 'critic_score', 'user_score', 'year_of_release', 'n_platforms'] menus = [] for axis, y_pos, columns_to_show in zip(['x', 'y'], [0.95, 0.8], [x_columns, y_columns]): buttons = [] for column in columns_to_show: button=dict( args=[{axis: [data[column]]}, {f'{axis}axis': {'title': column}}], label=column, method='update', ) buttons.append(button) menu = dict( buttons=buttons, direction='down', x=-0.14, y=y_pos, ) menus.append(menu) fig.update_layout( updatemenus=menus, title='Feature scatterplots', annotations=[dict(text='x axis:', x=-0.3, y=1.02, xref='paper', yref='paper', showarrow=False), dict(text='y axis:', x=-0.3, y=0.86, xref='paper', yref='paper', showarrow=False)], ) return figxxxxxxxxxxyears_slider = widgets.IntRangeSlider(value=[2013, 2016], min=1980, max=2016, continuous_update=False)interact(feature_scatterplots, data=widgets.fixed(data), years=years_slider, platform=[None] + top_platforms);xxxxxxxxxxdef sales_by_scores(data, years, sales): data = data.query("@years[0] <= year_of_release <= @years[1]") fig = make_subplots(1, 2, y_title=sales) fig.add_traces(px.scatter(data, x='user_score', y=sales).data, 1, 1) fig.add_traces(px.scatter(data, x='critic_score', y=sales).data, 1, 2) fig.update_layout(title_text='Sales by scores', title_x=0.5) fig.update_xaxes(title='User Score', row=1, col=1) fig.update_xaxes(title='Critics Score', row=1, col=2) return figxxxxxxxxxxyears_slider = widgets.IntRangeSlider(value=[2013, 2016], min=1980, max=2016, continuous_update=False)sales = ['total_sales', 'na_sales', 'eu_sales', 'jp_sales', 'other_sales']interact(sales_by_scores, data=widgets.fixed(data), years=years_slider, sales=sales);xxxxxxxxxxВидим, как похожи графики зависимости продаж от оценки критиков и пользователей.Видим, как похожи графики зависимости продаж от оценки критиков и пользователей.
xxxxxxxxxxДиаграмма рассеяния - хороший график, однако он не всегда хорошо отображает структуру данных. Например, график year_of_release - jp_sales не дает понять, какая между признаками зависимость. Более мощным инструментом является диаграмма рассения по корзинам. Чем меньше корзин, тем меньше вариативность (bias-variance tradeoff) и больше ошибка (условно MSE). 1 корзина - просто среднее по всем наблюдениям. Количество корзин = количество наблюдений - обычная диаграмма рассеяния.Диаграмма рассеяния - хороший график, однако он не всегда хорошо отображает структуру данных. Например, график year_of_release - jp_sales не дает понять, какая между признаками зависимость. Более мощным инструментом является диаграмма рассения по корзинам. Чем меньше корзин, тем меньше вариативность (bias-variance tradeoff) и больше ошибка (условно MSE). 1 корзина - просто среднее по всем наблюдениям. Количество корзин = количество наблюдений - обычная диаграмма рассеяния.
xxxxxxxxxxdef binned_scatterplot(data, years, x, y, platform, statistic, bins=10, trendline='lowess'): data = data.query('@years[1] >= year_of_release >= @years[0]') if platform: data = data.query('platform == @platform') data = data.dropna(subset=[x, y]) x_vals = data[x] y_vals = data[y] mean_y, bin_edges, bin_numbers = stats.binned_statistic(x_vals, y_vals, statistic, bins=bins) std_y, bin_edges, bin_numbers = stats.binned_statistic(x_vals, y_vals, 'std', bins=bins) fig = px.scatter(x=(bin_edges[:-1] + bin_edges[1:]) / 2, y=mean_y, error_y=std_y, trendline=trendline) fig.update_layout(xaxis_title=x, yaxis_title=y, title='Feature binned scatterplots') if trendline: fig.data[1].update(line_color='red') return figxxxxxxxxxxx_columns = ['user_score', 'critic_score', 'year_of_release', 'n_platforms', 'total_sales', 'na_sales', 'eu_sales', 'jp_sales', 'other_sales']y_columns = ['total_sales', 'na_sales', 'eu_sales', 'jp_sales', 'other_sales', 'critic_score', 'user_score', 'year_of_release', 'n_platforms']years_slider = widgets.IntRangeSlider(value=[2013, 2016], min=1980, max=2016, continuous_update=False)interact(binned_scatterplot, data=widgets.fixed(data), x=x_columns, y=y_columns, years=years_slider, platform=[None] + top_platforms, statistic=['mean', 'sum', 'median'], bins=(1, 20), trendline=['lowess', 'ols', None]);xxxxxxxxxxВидим, что со временем продажи в Северной Америке и Японии снизились, тем самым снизились и общие продажи. Однако немного выросли другие продажи. Последние 10 лет общие продажи примерно одинаковы.Также наблюдаем снижение оценки видеоигр самими пользователями с 2000 года. Оценка критиков же, наоборот, растет с 2007. Обе оценки положительно влияют на средние общие продажи. Только оценка критика начинает влиять на продажи только когда становится выше ~50. Последние 3 года оценка пользователей выше 4 не сильно влияет на продажи.Видим, что мультиплатформенные игры начали выпускаться начиная примерно с 2000, а большинство из них 2008-2010 года. Это говорит о том, что мультиплатформенность появилась относительно недавно. Удивительно, что чем больше поддерживаемых платформ, тем меньше оценка пользователей, но тем больше средние продажи. Посмотрим на продажи по платформам.Видим, что со временем продажи в Северной Америке и Японии снизились, тем самым снизились и общие продажи. Однако немного выросли другие продажи. Последние 10 лет общие продажи примерно одинаковы.
Также наблюдаем снижение оценки видеоигр самими пользователями с 2000 года. Оценка критиков же, наоборот, растет с 2007. Обе оценки положительно влияют на средние общие продажи. Только оценка критика начинает влиять на продажи только когда становится выше ~50. Последние 3 года оценка пользователей выше 4 не сильно влияет на продажи.
Видим, что мультиплатформенные игры начали выпускаться начиная примерно с 2000, а большинство из них 2008-2010 года. Это говорит о том, что мультиплатформенность появилась относительно недавно. Удивительно, что чем больше поддерживаемых платформ, тем меньше оценка пользователей, но тем больше средние продажи. Посмотрим на продажи по платформам.
xxxxxxxxxxРаспределения продаж похожи на нормальные, так что я сглажу точки нормальными распределениями.Распределения продаж похожи на нормальные, так что я сглажу точки нормальными распределениями.
xxxxxxxxxxnorm_pdf = lambda x, scale, mu, si: scale * stats.norm.pdf(x, mu, si)def normal_smoothing(y, return_distr=False): min_year = y.index.min() y = y.dropna() x = y.index params = curve_fit(norm_pdf, x, y, [5e5, 2003, 1.5])[0] y_hat = norm_pdf(np.arange(min_year, 2017, 0.25), *params) if return_distr: return pd.Series(y_hat, index=np.arange(min_year, 2017, 0.25)), params[1:] return pd.Series(y_hat, index=np.arange(min_year, 2017, 0.25))xxxxxxxxxxdef sales_by_year(data, years, platforms, smooth=True): data = data.query('@years[0] <= year_of_release <= @years[1]') top_platforms = data.pivot_table(index='platform', values='total_sales', aggfunc='sum') top_platforms = top_platforms.sort_values(by='total_sales', ascending=False)[:platforms].index top_platforms_data = data.query("platform in @top_platforms") top_platforms_sales = top_platforms_data.pivot_table(index='year_of_release', columns='platform', values='total_sales', aggfunc='sum').reindex(np.arange(years[0], 2018)) if smooth: try: scatterplot = px.scatter(top_platforms_sales.melt(ignore_index=False).reset_index(), x='year_of_release', y='value', color='platform') sales_smoothed = top_platforms_sales.reindex(np.arange(years[0]-3, 2017, 0.25)).apply(normal_smoothing) sales_smoothed[sales_smoothed < 1] = np.nan lineplot = px.line(sales_smoothed.melt(ignore_index=False).reset_index(), x='index', y='value', color='platform', render_mode='webg1') # to avoid problems with rangeslider lineplot.add_traces(scatterplot.data) except Exception: print('\n Could not smooth the lines') lineplot = px.line(top_platforms_sales.melt(ignore_index=False).reset_index(), x='year_of_release', y='value', color='platform') else: lineplot = px.line(top_platforms_sales.melt(ignore_index=False).reset_index(), x='year_of_release', y='value', color='platform') lineplot.update_layout(xaxis_title='Year', yaxis_title='Total sales', xaxis_rangeslider_visible=True, title='Platforms sales') return lineplotxxxxxxxxxxyears_slider = widgets.IntRangeSlider(value=[1995, 2016], min=1980, max=2016, continuous_update=False)interact(sales_by_year, data=widgets.fixed(data), years=years_slider, platforms=(1, 10), smooth=[True, False]);xxxxxxxxxx# to create variables locallydef get_platforms_sales_from_2010(data): top_platforms = (data .query('year_of_release > 2010') .pivot_table(index='platform', values='total_sales', aggfunc='sum') .sort_values(by='total_sales', ascending=False)[:5].index ) top_platforms_sales = (data .query("platform in @top_platforms") .pivot_table(index='year_of_release', columns='platform', values='total_sales', aggfunc='sum') .reindex(np.arange(2010, 2018)) .melt(ignore_index=False).reset_index() ) fig = px.line(top_platforms_sales, x='year_of_release', y='value', color='platform') fig.update_layout(xaxis_title='Year', yaxis_title='Total sales', title='Platforms sales from 2010s') return figxxxxxxxxxxget_platforms_sales_from_2010(data)xxxxxxxxxxdef sales_by_platform(data, platform, smooth=True): data = data.query('platform == @platform') sales = data.groupby('year_of_release')['total_sales'].sum() min_year = int(sales.index.min()) sales = sales.reindex(range(min_year-3, 2017)) if smooth: try: scatterplot = px.scatter(x=sales.index, y=sales) sales_smoothed, (mu, std) = normal_smoothing(sales.reindex(np.arange(min_year-3, 2017, 0.5)), return_distr=True) lineplot = px.line(x=sales_smoothed.index, y=sales_smoothed) lineplot.add_traces(scatterplot.data) print('\n 95% interval:', round(mu, 2), '+-', round(2*std, 2)) except Exception as err: print('\n Could not smooth the line') lineplot = px.line(x=sales.index, y=sales) else: lineplot = px.line(x=sales.index, y=sales.values) lineplot.update_layout(xaxis_title='year', yaxis_title='sales', xaxis_rangeslider_visible=True, title=platform + ' sales') return lineplotxxxxxxxxxxinteract(sales_by_platform, data=widgets.fixed(data), platform=top_platforms, smooth=[True, False]);xxxxxxxxxxКак видим, продажи по годам достаточно хорошо описываются нормальными распределениями. Сами платформы в среднем живут от 6 до 10 лет, а пик продаваемости приходится на 3-5 год. Также из графиков видно, что большинство платформ уже отжили свое, а из новых только PS4 и XOne. Также у платформы DS наблюдается странный выброс - игра 1985 года, хотя игры на DS выходили преимущественно уже после 2000-х.Как видим, продажи по годам достаточно хорошо описываются нормальными распределениями. Сами платформы в среднем живут от 6 до 10 лет, а пик продаваемости приходится на 3-5 год. Также из графиков видно, что большинство платформ уже отжили свое, а из новых только PS4 и XOne. Также у платформы DS наблюдается странный выброс - игра 1985 года, хотя игры на DS выходили преимущественно уже после 2000-х.
xxxxxxxxxxdata.query("platform == 'DS' and year_of_release == 1985")xxxxxxxxxxУдалим его.Удалим его.
xxxxxxxxxxdata = data.drop(15229).reset_index(drop=True)xxxxxxxxxxdef feature_boxplots(data, years, platforms): data = data.query('@years[1] >= year_of_release >= @years[0]') top_platforms = data.pivot_table(index='platform', values='total_sales', aggfunc='sum') top_platforms = top_platforms.sort_values(by='total_sales', ascending=False)[:platforms].index top_platforms_data = data.query("platform in @top_platforms") fig = px.box(top_platforms_data, x='platform', y='total_sales', category_orders={'platform': top_platforms}) category_orders = {'platform': top_platforms, 'genre': data['genre'].value_counts().index, 'decade_of_release': ['1980s', '1990s', '2000s', '2010s'], 'n_platforms': list(range(12)), 'rating': data['rating'].value_counts().index} menus = [] buttons = [] for column in ['platform', 'genre', 'decade_of_release', 'n_platforms', 'rating']: button=dict( args=[{'x': [top_platforms_data[column] if column == 'platform' else data[column]]}, {'xaxis': {'title': column, 'categoryarray': category_orders[column]}}], label=column, method='update', ) buttons.append(button) menu = dict( buttons=buttons, direction='down', x=-0.14, y=0.95, ) menus.append(menu) columns_to_show = ['total_sales', 'na_sales', 'eu_sales', 'jp_sales', 'other_sales', 'critic_score', 'user_score', 'year_of_release', 'n_platforms'] buttons = [] for column in columns_to_show: button=dict( args=[{'y': [top_platforms_data[column] if fig.layout.xaxis.title.text == 'platform' else data[column]]}, {'yaxis': {'title': column}}], label=column, method='update', ) buttons.append(button) menu = dict( buttons=buttons, direction='down', x=-0.155, y=0.8, ) menus.append(menu) fig.update_layout( updatemenus=menus, title=f'Feature boxplots', annotations=[dict(text='x axis:', x=-0.32, y=1.02, xref='paper', yref='paper', showarrow=False), dict(text='y axis:', x=-0.32, y=0.86, xref='paper', yref='paper', showarrow=False)], ) return figxxxxxxxxxx# platforms slider is only for "platform" as x axis platforms_slider = widgets.IntSlider(value=10, min=1, max=20, continuous_update=False)years_slider = widgets.IntRangeSlider(value=[2013, 2016], min=1980, max=2016, continuous_update=False)interact(feature_boxplots, data=widgets.fixed(data), years=years_slider, platforms=platforms_slider);xxxxxxxxxxСреди самых продаваемых платформ за все время (platforms = 10) у PS3 и X360 общие продажи имеют большее среднее, чем у остальных платформ. Потом идут PS, PS2 и Wii. В последние 3 года высокие продажи имеют также PS4 и XOne. Самые высокую медианну по продажам имеют платформы 2600, SNES и N64, однако у них не так много игр (меньше общие продажи). Хуже всего продаются игры на PC. Однако, как ни странно, там самая высокая средняя оценка критиков. Возможно, это связано с тем, что у большинства людей игровые консоли, а не ПК. Высоко критиками также оценены Wii и XOne. Пользователи в последние 3 года же выше оценивают PSV, DS, 3DS, PS4 и PS3.Среди жанров по продажам лидируют шутеры, за ними платформеры и спортивные игры. Хуже всего продаются приключения, стратегии и пазлы. Но там, как ни странно, самая высокая оценка пользователей, а у шутеров и спортивных игр низкая. Можно сделать вывод, что люди не очень любят думать и больше любят то, что в реальной жизни они никогда не сделают. Чем выше количество платформ, тем выше средние продажи. У игр с рейтингом M и E10+ чуть выше продажи.Разберем подробнее каждый регион.Среди самых продаваемых платформ за все время (platforms = 10) у PS3 и X360 общие продажи имеют большее среднее, чем у остальных платформ. Потом идут PS, PS2 и Wii. В последние 3 года высокие продажи имеют также PS4 и XOne. Самые высокую медианну по продажам имеют платформы 2600, SNES и N64, однако у них не так много игр (меньше общие продажи). Хуже всего продаются игры на PC. Однако, как ни странно, там самая высокая средняя оценка критиков. Возможно, это связано с тем, что у большинства людей игровые консоли, а не ПК. Высоко критиками также оценены Wii и XOne. Пользователи в последние 3 года же выше оценивают PSV, DS, 3DS, PS4 и PS3.
Среди жанров по продажам лидируют шутеры, за ними платформеры и спортивные игры. Хуже всего продаются приключения, стратегии и пазлы. Но там, как ни странно, самая высокая оценка пользователей, а у шутеров и спортивных игр низкая. Можно сделать вывод, что люди не очень любят думать и больше любят то, что в реальной жизни они никогда не сделают.
Чем выше количество платформ, тем выше средние продажи.
У игр с рейтингом M и E10+ чуть выше продажи.
Разберем подробнее каждый регион.
xxxxxxxxxxdef feature_pie_chart(data, years, by, values, statistic='mean'): data = data.query('@years[0] <= year_of_release <= @years[1]') pie_data = (data .pivot_table(index=by, values=values, aggfunc=statistic) .sort_values(by=values, ascending=False) .reset_index()) fig = px.pie(pie_data, values=values, names=by, title='Feature pie chart') fig.update_traces(textposition='inside', textinfo='label+percent') fig.update_layout(showlegend=False) return figxxxxxxxxxxcat_columns = ['platform', 'genre', 'decade_of_release', 'n_platforms', 'rating']num_columns = ['na_sales', 'eu_sales', 'jp_sales', 'other_sales', 'critic_score', 'user_score', 'total_sales']years_slider = widgets.IntRangeSlider(value=[2013, 2016], min=1980, max=2016, continuous_update=False)interact(feature_pie_chart, data=widgets.fixed(data), by=cat_columns, values=num_columns, years=years_slider, statistic=['mean', 'sum', 'median']);xxxxxxxxxxdef region_sales(data, years, by, statistic='mean'): fig = make_subplots(1, 3, subplot_titles=['North America', 'European Union', 'Japan'], specs=[[{'type': 'domain'}]*3]) for i, region in enumerate(['na', 'eu', 'jp']): region_data = (data .query("@years[0] <= year_of_release <= @years[1]") .pivot_table(index=by, values=f'{region}_sales', aggfunc=statistic) .sort_values(by=f'{region}_sales', ascending=False)[:5] .reset_index()) region_fig = px.pie(region_data, names=by, values=f'{region}_sales') region_fig.update_traces(textposition='inside', textinfo='label+percent') fig.add_traces(region_fig.data, 1, i+1) fig.update_layout(title=f'Sales by {by}', showlegend=False, height=430) return figxxxxxxxxxxyears_slider = widgets.IntRangeSlider(value=[2013, 2016], min=1980, max=2016, continuous_update=False)interact(region_sales, data=widgets.fixed(data), years=years_slider, by=['platform', 'genre', 'rating'], statistic=['mean', 'sum', 'median']);xxxxxxxxxxdef region_preferences(data, years, region, statistic='mean'): region_sales_dict = {'North America': 'na_sales', 'European Union': 'eu_sales', 'Japan': 'jp_sales'} fig = make_subplots(1, 3, subplot_titles=['Platform', 'Genre', 'Rating'], specs=[[{'type': 'domain'}]*3]) for i, index in enumerate(['platform', 'genre', 'rating']): region_stats = (data .query("@years[0] <= year_of_release <= @years[1]") .pivot_table(index=index, values=region_sales_dict[region], aggfunc=statistic) .sort_values(by=region_sales_dict[region], ascending=False)[:5] .reset_index()) region_fig = px.pie(region_stats, names=index, values=region_sales_dict[region]) region_fig.update_traces(textposition='inside', textinfo='label+percent') fig.add_traces(region_fig.data, 1, i+1) fig.update_layout(title=region + ' sales by', showlegend=False, height=430) return figxxxxxxxxxxyears_slider = widgets.IntRangeSlider(value=[2013, 2016], min=1980, max=2016, continuous_update=False)regions = ['North America', 'European Union', 'Japan']interact(region_preferences, data=widgets.fixed(data), years=years_slider, region=regions, statistic=['mean', 'sum', 'median']);xxxxxxxxxx**North America:*** В последние 3-5 лет в основном популярны XOne, X360, Wii и PS4. У X360 большие медианные продажи, но относительно невысокие совокупные. Это значит, что игры на X360 еще хорошо продаются (не многие, возможно, успели перейти на более новые консоли), но игр стали производить меньше на эту платформу. * Предпочтения жанров такие же, как и общие. Популярны шутеры, платформеры, спортивные игры, файтеры и гонки. Cтратегии, пазлы, приключения и ролевые игры практически не пользуются спросом. Опять же самая высокая медианна у жанра шутер, но там не самые высокие совокупные продажи. Просто игр этого жанра делают меньше.* Последние 3 года у рейтингов М и E10+ выше медианные продажи, а у Т и E меньше. Однако за весь период у них примерно одинаковые продажи. У игр без рейтинга невысокие продажи.**European Union:*** Медианные продажи выше у Wii, DS, X360 и XOne. Потом с небольшим отрывом идут PS3 и PS4. В принципе, это ожидаемо, так как выше мы видели, что среди платформ почти все платформы уже канули в небытие, продажи растут только у PS4 и XOne. * Жанровые предпочтения в Европе такие же как в Северной Америке.* Так же высоко оцениваются игры с рейтингом М. Игры с рейтингом E10+ тоже имеют медианные продажи выше, чем у рейтингов Т и Е. Такая тенденция прослеживается всегда.**Japan:*** Вот у японцев что-то интересное. Несмотря на падение общих продаж, в Японии последние 10 лет все равно лидируют игры на PSV, 3DS, PSP и PS3. В Xbox они вообще не играют. * Японцы предпочитают ролевые игры, файтинг, пазлы и стратегии. Они почти не играют в шутеры и гонки.* Высокие средние и общие продажи у игр без рейтинга. Игры с рейтингом Т имеют тоже продажи выше, чем игры с рейтингом M. Игры с рейтингом Е10+ менее популярны в Японии.В целом европейцы и жители Северной Америки схожи в предпочтениях. Они играют на современных настольных консолях в шутеры, гонки и платформеры. Предпочитают игры с рейтингом M или E10+. Японцы же играют в портативные консоли (PSP, PSV, 3DS), и играют в основном в ролевые игры, файтинг и стратегии. Для них предпочтительнее игры без рейтинга и рейтинг T.North America:
В последние 3-5 лет в основном популярны XOne, X360, Wii и PS4. У X360 большие медианные продажи, но относительно невысокие совокупные. Это значит, что игры на X360 еще хорошо продаются (не многие, возможно, успели перейти на более новые консоли), но игр стали производить меньше на эту платформу.
Предпочтения жанров такие же, как и общие. Популярны шутеры, платформеры, спортивные игры, файтеры и гонки. Cтратегии, пазлы, приключения и ролевые игры практически не пользуются спросом. Опять же самая высокая медианна у жанра шутер, но там не самые высокие совокупные продажи. Просто игр этого жанра делают меньше.
Последние 3 года у рейтингов М и E10+ выше медианные продажи, а у Т и E меньше. Однако за весь период у них примерно одинаковые продажи. У игр без рейтинга невысокие продажи.
European Union:
Медианные продажи выше у Wii, DS, X360 и XOne. Потом с небольшим отрывом идут PS3 и PS4. В принципе, это ожидаемо, так как выше мы видели, что среди платформ почти все платформы уже канули в небытие, продажи растут только у PS4 и XOne.
Жанровые предпочтения в Европе такие же как в Северной Америке.
Так же высоко оцениваются игры с рейтингом М. Игры с рейтингом E10+ тоже имеют медианные продажи выше, чем у рейтингов Т и Е. Такая тенденция прослеживается всегда.
Japan:
Вот у японцев что-то интересное. Несмотря на падение общих продаж, в Японии последние 10 лет все равно лидируют игры на PSV, 3DS, PSP и PS3. В Xbox они вообще не играют.
Японцы предпочитают ролевые игры, файтинг, пазлы и стратегии. Они почти не играют в шутеры и гонки.
Высокие средние и общие продажи у игр без рейтинга. Игры с рейтингом Т имеют тоже продажи выше, чем игры с рейтингом M. Игры с рейтингом Е10+ менее популярны в Японии.
В целом европейцы и жители Северной Америки схожи в предпочтениях. Они играют на современных настольных консолях в шутеры, гонки и платформеры. Предпочитают игры с рейтингом M или E10+. Японцы же играют в портативные консоли (PSP, PSV, 3DS), и играют в основном в ролевые игры, файтинг и стратегии. Для них предпочтительнее игры без рейтинга и рейтинг T.
xxxxxxxxxx## Средние пользовательские рейтинги платформ Xbox One и PC одинаковыеxxxxxxxxxx**Нулевая гипотеза:** Средние рейтинги платформ XOne и PC одинаковые.**Альтернативная гипотеза:** Средние рейтинги отличаются.Нулевая гипотеза: Средние рейтинги платформ XOne и PC одинаковые.
Альтернативная гипотеза: Средние рейтинги отличаются.
xxxxxxxxxxdef platform_ttest(data, years): xone_score = data.query("@years[1] >= year_of_release >= @years[0] and platform == 'XOne'")['user_score'].dropna() pc_score = data.query("@years[1] >= year_of_release >= @years[0] and platform == 'PC'")['user_score'].dropna() fig = ff.create_distplot([xone_score, pc_score], ['XOne', 'PC'], show_hist=False, show_rug=False, colors=['red', 'blue']) fig.update_layout(xaxis_title='User score', yaxis_title='Probability density', title='User scores for PC and XOne') #means fig.add_traces(px.line(x=[pc_score.mean()]*2, y=[0, stats.gaussian_kde(pc_score)(pc_score.mean()).item()], color_discrete_sequence=['purple']).data) fig.add_traces(px.line(x=[xone_score.mean()]*2, y=[0, stats.gaussian_kde(xone_score)(xone_score.mean()).item()], color_discrete_sequence=['purple']).data) fig.show() print('P-value:', round(stats.ttest_ind(xone_score, pc_score, equal_var=False).pvalue, 4))xxxxxxxxxxyears_slider = widgets.IntRangeSlider(value=[2013, 2016], min=1980, max=2016, continuous_update=False)interact(platform_ttest, data=widgets.fixed(data), years=years_slider);xxxxxxxxxxP-значение недостаточно мало, чтобы сказать, что разность в средних статистически значима и отвергнуть нулевую гипотезу. Я хочу посчитать p-значение вручную.P-значение недостаточно мало, чтобы сказать, что разность в средних статистически значима и отвергнуть нулевую гипотезу. Я хочу посчитать p-значение вручную.
xxxxxxxxxxxone_score = data.query("2016 >= year_of_release >= 2013 and platform == 'XOne'")['user_score'].dropna()pc_score = data.query("2016 >= year_of_release >= 2013 and platform == 'PC'")['user_score'].dropna()sem = np.sqrt(xone_score.sem()**2 + pc_score.sem()**2)x = abs(xone_score.mean() - pc_score.mean()) / sem2 * (1 - stats.t.cdf(x, len(xone_score) + len(pc_score) - 2))xxxxxxxxxxНе знаю, откуда эта неточность.Не знаю, откуда эта неточность.
xxxxxxxxxx## Средние пользовательские рейтинги жанров Action и Sports разные.xxxxxxxxxx**Нулевая гипотеза:** Средние рейтинги жанров Action и Sports одинаковые.**Альтернативная гипотеза:** Средние рейтинги отличаются.Нулевая гипотеза: Средние рейтинги жанров Action и Sports одинаковые.
Альтернативная гипотеза: Средние рейтинги отличаются.
xxxxxxxxxxdef genre_ttest(data, years): action_score = data.query("@years[1] >= year_of_release >= @years[0] and genre == 'Action'")['user_score'].dropna() sports_score = data.query("@years[1] >= year_of_release >= @years[0] and genre == 'Sports'")['user_score'].dropna() fig = ff.create_distplot([action_score, sports_score], ['Action', 'Sports'], show_hist=False, show_rug=False, colors=['red', 'blue']) fig.update_layout(xaxis_title='User score', yaxis_title='Probability density', title='User scores for Action and Sports genres') # means fig.add_traces(px.line(x=[action_score.mean()]*2, y=[0, stats.gaussian_kde(action_score)(action_score.mean()).item()], color_discrete_sequence=['purple']).data) fig.add_traces(px.line(x=[sports_score.mean()]*2, y=[0, stats.gaussian_kde(sports_score)(sports_score.mean()).item()], color_discrete_sequence=['purple']).data) fig.show() print('P-value:', round(stats.ttest_ind(action_score, sports_score, equal_var=False).pvalue, 4))xxxxxxxxxxyears_slider = widgets.IntRangeSlider(value=[2013, 2016], min=1980, max=2016, continuous_update=False)interact(genre_ttest, data=widgets.fixed(data), years=years_slider);xxxxxxxxxxP-значение очень мало, так что можно отвергнуть нулевую гипотезу и принять альтернативную. Значит, разность между средними рейтингами игр жанра action и sport статистически значима.P-значение очень мало, так что можно отвергнуть нулевую гипотезу и принять альтернативную. Значит, разность между средними рейтингами игр жанра action и sport статистически значима.
xxxxxxxxxxaction_score = data.query("2016 >= year_of_release >= 2013 and genre == 'Action'")['user_score'].dropna()sports_score = data.query("2016 >= year_of_release >= 2013 and genre == 'Sports'")['user_score'].dropna() sem = pd.concat((action_score, sports_score)).std() * np.sqrt(1/len(action_score) + 1/len(sports_score))x = abs(action_score.mean() - sports_score.mean()) / sem2 * (1 - stats.t.cdf(x, len(action_score) + len(sports_score) - 2))xxxxxxxxxxПроблема проведенного выше анализа в том, что он не дает понять, какой признак является наиболее сильным. И оценки критиков и игроков, и платформа, и жанр, и рейтинг влияют на количество проданных копий. Но что влияет больше? Для того чтобы измерить важность признаков, мы обучим какую-нибудь модель, а потом посмотрим, какие признаки больше всего помогли ей в предсказании продаж. Для этого я использовал библиотеку градиентного бустинга от Яндекса - Catboost. Сначала рассмотрим весь промежуток времени.Проблема проведенного выше анализа в том, что он не дает понять, какой признак является наиболее сильным. И оценки критиков и игроков, и платформа, и жанр, и рейтинг влияют на количество проданных копий. Но что влияет больше? Для того чтобы измерить важность признаков, мы обучим какую-нибудь модель, а потом посмотрим, какие признаки больше всего помогли ей в предсказании продаж. Для этого я использовал библиотеку градиентного бустинга от Яндекса - Catboost. Сначала рассмотрим весь промежуток времени.
xxxxxxxxxxX = data.drop(columns=['name', 'decade_of_release', 'na_sales', 'eu_sales', 'jp_sales', 'other_sales', 'total_sales'])y = data[['na_sales', 'eu_sales', 'jp_sales', 'other_sales', 'total_sales']]cat_features = X.select_dtypes(exclude='number').columns.valuesxxxxxxxxxxX['rating'].fillna('nan', inplace=True)X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)pool = Pool(X, y, cat_features)train_pool = Pool(X_train, y_train, cat_features)test_pool = Pool(X_test, y_test, cat_features)xxxxxxxxxxЧем лучше модель предсказывает продажи, тем более надежны выданные ею важности признаков. Чтобы посмотреть на среднее качество модели, я проведу кросс-валидацию. То есть разобью генеральную совокупность на выборки, и на этих выборках обучу и оценю качество модели. Потом уже обучу финальную модель.Чем лучше модель предсказывает продажи, тем более надежны выданные ею важности признаков. Чтобы посмотреть на среднее качество модели, я проведу кросс-валидацию. То есть разобью генеральную совокупность на выборки, и на этих выборках обучу и оценю качество модели. Потом уже обучу финальную модель.
xxxxxxxxxxcv_params = dict( objective='MultiRMSE', iterations=5000, early_stopping_rounds=500, one_hot_max_size=100,)folds = ShuffleSplit(test_size=0.2)xxxxxxxxxxcv( pool, cv_params, folds=folds, logging_level='Silent', plot=True,);xxxxxxxxxxГрафик выше показывает среднюю квадратичную ошибку в зависимости от количества решающих деревьев в модели для 3 моделей, обученных на разных выборках. Чем ниже ошибка, тем лучше. Как видим, качество модели немало зависит от разделения на обучающую и тестовую выборку.График выше показывает среднюю квадратичную ошибку в зависимости от количества решающих деревьев в модели для 3 моделей, обученных на разных выборках. Чем ниже ошибка, тем лучше. Как видим, качество модели немало зависит от разделения на обучающую и тестовую выборку.
xxxxxxxxxxmodel = CatBoostRegressor(**cv_params)model.fit( train_pool, eval_set=test_pool, silent=True, plot=True,);xxxxxxxxxxy_hat = pd.DataFrame(model.predict(test_pool), columns=y.columns)y_hat['total_sales_separate'] = y_hat.sum(axis=1) - y_hat['total_sales']y_hat[y_hat < 0] = 0y_hat.head()xxxxxxxxxxy_test['total_sales_separate'] = y_test['total_sales']metrics = {'R2': r2_score(y_test, y_hat, multioutput='raw_values'), 'MAE': mean_absolute_error(y_test, y_hat, multioutput='raw_values'), 'MAE_scaled': mean_absolute_error(y_test, y_hat, multioutput='raw_values') / y_test.std()}metrics = pd.DataFrame(metrics, index=y_test.columns)xxxxxxxxxxwith pd.option_context('display.float_format', '{:,.4f}'.format): display(metrics)xxxxxxxxxxМодель на примерно на 30% лучше простого среднего, а ее предсказания в среднем ошибаются на половину стандартного отклонения. По качеству предсказаний нет разницы между предсказывать total_sales сразу или сначала na_sales, ..., other_sales, а потом их суммировать.Модель на примерно на 30% лучше простого среднего, а ее предсказания в среднем ошибаются на половину стандартного отклонения. По качеству предсказаний нет разницы между предсказывать total_sales сразу или сначала na_sales, ..., other_sales, а потом их суммировать.
xxxxxxxxxxmodel.get_feature_importance(test_pool, prettified=True)xxxxxxxxxxДанные значения важности показывают, как сильно изменятся предсказания, если изменится признак. Catboost использует свой алгоритм подсчета важностей признаков, который достаточно плохо интерпретируется. Поэтому я посмотрю еще на важности признаков при простом перемешивании значений признака.Данные значения важности показывают, как сильно изменятся предсказания, если изменится признак. Catboost использует свой алгоритм подсчета важностей признаков, который достаточно плохо интерпретируется. Поэтому я посмотрю еще на важности признаков при простом перемешивании значений признака.
xxxxxxxxxxperm_results = permutation_importance(model, X_test, y_test.drop('total_sales_separate', axis=1), n_jobs=-1)perm_results.pop('importances')pd.DataFrame(perm_results, index=X.columns).sort_values(by='importances_mean', ascending=False)xxxxxxxxxxОба метода показывают, что самыми важными признаками являются оценка критиков и платформа игры. Меньше всего важны рейтинг и оценка пользователей (как ни странно). Возможно, оценка пользователей коррелирует с оценкой критиков (0.59), и поэтому мало важна для модели. Посмотрим на shap-значения. Грубо говоря, shap-значения показывают, какой признак сколько внес в итоговое предсказание, так что чем больше абсолютное shap-значение, тем лучше.Оба метода показывают, что самыми важными признаками являются оценка критиков и платформа игры. Меньше всего важны рейтинг и оценка пользователей (как ни странно). Возможно, оценка пользователей коррелирует с оценкой критиков (0.59), и поэтому мало важна для модели. Посмотрим на shap-значения. Грубо говоря, shap-значения показывают, какой признак сколько внес в итоговое предсказание, так что чем больше абсолютное shap-значение, тем лучше.
xxxxxxxxxxshap_values = model.get_feature_importance(test_pool, type='ShapValues', shap_calc_type='Exact')expected_values = shap_values[0, :, -1]shap_values = shap_values[:, :, :-1]xxxxxxxxxxsales = {column: i for i, column in enumerate(y.columns)}summary_plot = lambda shap_values, X, y: shap.summary_plot(shap_values[:, sales[y]], X)xxxxxxxxxxinteract(summary_plot, shap_values=widgets.fixed(shap_values), X=widgets.fixed(X_test), y=y.columns);xxxxxxxxxxCatBoost переводит категориальные признаки в хеши, и их числовые значения не имеют значения, поэтому категориальные признаки на графике окрашены в серый. Видим, что чем больше оценка критиков, тем выше shap-значения. Чем меньше количество поддерживаемых платформ, тем меньше shap-значения.CatBoost переводит категориальные признаки в хеши, и их числовые значения не имеют значения, поэтому категориальные признаки на графике окрашены в серый. Видим, что чем больше оценка критиков, тем выше shap-значения. Чем меньше количество поддерживаемых платформ, тем меньше shap-значения.
xxxxxxxxxxdependence_plot = lambda shap_values, X, y, column: shap.dependence_plot(column, shap_values[:, sales[y]], X)interact(dependence_plot, shap_values=widgets.fixed(shap_values), X=widgets.fixed(X_test), y=y.columns, column=X.columns);xxxxxxxxxxИз графиков мы видим примерно то же самое, что видели из диаграм рассеяния и боксплотов. Видно, что PC преимущественно влияет негативно на продажи, а платформы PS2/3/4, Wii и X360/XOne - положительно. Шутеры, платформеры и файтинги увеличивают shap-значения, приключения, стратегии и пазлы уменьшают. Также shap-значения повышают симуляторы. Рейтинги E10+ и T понижают продажи, а M и Е повышают. Так же как было видно на диаграмме рассеяния, оценка критиков начинает влиять только после ~60. Однако, это показатели за все время. Наша задача предсказать продажи в следующих годах. Какой промежуток времени для анализа выбрать? Из графиков наверху (повторил внизу) мы видели, что платформы живут ~10 лет, а на текущий момент почти все крупные платформы уходят с рынка, потому что появились новые платформы PS4 и XOne. То есть сейчас идет начало цикла. Для того, чтобы предсказать, что будет в середине цикла, надо взять уже прошедший цикл и посмотреть на нем важность признаков. Я выберу цикл с 2005 года, когда появились X360, DS, Wii, PS3, PSP, по текущий момент, когда все эти платформы уже изжили себя. Из графиков мы видим примерно то же самое, что видели из диаграм рассеяния и боксплотов. Видно, что PC преимущественно влияет негативно на продажи, а платформы PS2/3/4, Wii и X360/XOne - положительно. Шутеры, платформеры и файтинги увеличивают shap-значения, приключения, стратегии и пазлы уменьшают. Также shap-значения повышают симуляторы. Рейтинги E10+ и T понижают продажи, а M и Е повышают. Так же как было видно на диаграмме рассеяния, оценка критиков начинает влиять только после ~60. Однако, это показатели за все время. Наша задача предсказать продажи в следующих годах. Какой промежуток времени для анализа выбрать? Из графиков наверху (повторил внизу) мы видели, что платформы живут ~10 лет, а на текущий момент почти все крупные платформы уходят с рынка, потому что появились новые платформы PS4 и XOne. То есть сейчас идет начало цикла. Для того, чтобы предсказать, что будет в середине цикла, надо взять уже прошедший цикл и посмотреть на нем важность признаков. Я выберу цикл с 2005 года, когда появились X360, DS, Wii, PS3, PSP, по текущий момент, когда все эти платформы уже изжили себя.
xxxxxxxxxxyears_slider = widgets.IntRangeSlider(value=[2000, 2016], min=1980, max=2016, continuous_update=False)interact(sales_by_year, data=widgets.fixed(data), years=years_slider, platforms=(1, 10), smooth=[True, False]);xxxxxxxxxxdef get_feature_importance(data, years, with_shap=False): data = data.query("@years[0] <= year_of_release <= @years[1]") X = data.drop(columns=['name', 'decade_of_release', 'na_sales', 'eu_sales', 'jp_sales', 'other_sales', 'total_sales']) y = data[['na_sales', 'eu_sales', 'jp_sales', 'other_sales', 'total_sales']] X['rating'].fillna('nan', inplace=True) X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2) pool = Pool(X, y, cat_features) train_pool = Pool(X_train, y_train, cat_features) test_pool = Pool(X_test, y_test, cat_features) model = CatBoostRegressor(**cv_params) model.fit( train_pool, eval_set=test_pool, silent=True, plot=True, ) y_hat = pd.DataFrame(model.predict(test_pool), columns=y.columns) y_hat[y_hat < 0] = 0 metrics = {'R2': r2_score(y_test, y_hat, multioutput='raw_values'), 'MAE': mean_absolute_error(y_test, y_hat, multioutput='raw_values'), 'MAE_scaled': mean_absolute_error(y_test, y_hat, multioutput='raw_values') / y_test.std()} metrics = pd.DataFrame(metrics, index=y_test.columns) perm_results = permutation_importance(model, X_test, y_test, n_jobs=-1) perm_results.pop('importances') perm_results = pd.DataFrame(perm_results, index=X.columns).sort_values(by='importances_mean', ascending=False) with pd.option_context('display.float_format', '{:,.4f}'.format): print('METRICS') display(metrics) print('MODEL FEATURE IMPORTANCES') display(model.get_feature_importance(test_pool, prettified=True)) print('PERMUTATION IMPORTANCE') display(perm_results) if with_shap: shap_values = model.get_feature_importance(test_pool, type='ShapValues') expected_values = shap_values[0, :, -1] shap_values = shap_values[:, :, :-1] interact(summary_plot, shap_values=widgets.fixed(shap_values), X=widgets.fixed(X_test), y=y.columns) interact(dependence_plot, shap_values=widgets.fixed(shap_values), X=widgets.fixed(X_test), y=y.columns, column=X.columns)years_slider = widgets.IntRangeSlider(value=[2005, 2016], min=1980, max=2016, continuous_update=False)interact_manual(get_feature_importance, data=widgets.fixed(data), years=years_slider, with_shap=[False, True]);xxxxxxxxxxДля последних 15 лет результаты предсказания немного лучше для Северной Америки и Европы, но хуже для Японии. Важными признаками остались оценка критиков и платформа. Важным стал жанр игры. Также количество поддерживаемых платформ стало больше значить. Рейтинг и оценка пользователей остались маловажными признаками, и перестал быть важным год выпуска (что вполне логично для периода в 15 лет).Для последних 15 лет результаты предсказания немного лучше для Северной Америки и Европы, но хуже для Японии. Важными признаками остались оценка критиков и платформа. Важным стал жанр игры. Также количество поддерживаемых платформ стало больше значить. Рейтинг и оценка пользователей остались маловажными признаками, и перестал быть важным год выпуска (что вполне логично для периода в 15 лет).
xxxxxxxxxxЕсли обучить модель на данных последних 3 лет, то получим примерно такие же результаты, что значит, что важность оценок критиков и платформы мало зависят от времени и почти всегда являются одними из главных признаков.Если обучить модель на данных последних 3 лет, то получим примерно такие же результаты, что значит, что важность оценок критиков и платформы мало зависят от времени и почти всегда являются одними из главных признаков.
xxxxxxxxxxНаболее сильными признаками являются оценка критиков и платформа игры. За ними идут количество поддерживаемых платформ и жанр. Год выпуска, рейтинг и оценка пользователей мало влияют на продажи (продажи зависят от оценки пользователей, но оценка критиков лучше моделирует продажи, а оценка пользователей практически не превносит никакой новой информации).**За все время:*** Оценка критиков начинает сильно положительно влиять на продажи только когда выше ~60.* Самыми продаваемыми платформами являются PS2/3/4, X360/XOne и Wii. Хуже всего продаются компьютерные игры. Платформы в среднем живут 6-10 лет и пик продаж наступает на 3-5 год.* Общие продажи снизились с 1980 года и последние 10 лет примерно одинаковы (относительно всего периода).* Чем выше количество поддерживаемых платформ, тем больше продажи.* Лучше всего продаются шутеры, платформеры и гонки, хуже всего - приключения, стратегии и пазлы.* Чем выше оценка пользователей, тем выше продажи.* Игры с рейтингом M продаются чуть лучше.**За последние 5 лет:*** За последние 5 лет виден спад в продажах видеоигр.* Почти все крупные платформы уже отжили свое, продажи растут только у новых консолей PS4 и XOne.* Платформеры и гонки стали чуть хуже продаваться.**По регионам:**Население Европы и Северной Америки имеют схожие вкусы. Они играют на современных настольных консолях в шутеры, гонки и платформеры. Предпочитают игры с рейтингом M или E10+. Японцы же играют в портативные консоли (PSP, PSV, 3DS), и играют в основном в ролевые игры, файтинг и стратегии. Для них предпочтительнее игры без рейтинга и игры с рейтингом T.**Гипотезы:*** Не смогли отвергнуть нулевую гипотезу о том, что средние рейтинги платформ XOne и PC одинаковые.* Показали, что вероятность, что средние пользовательские рейтинги жанров Action и Sports одинаковые - крайне мала. Мы принимаем альтернативную гипотезу о том, что средние рейтинги отличаются.Наболее сильными признаками являются оценка критиков и платформа игры. За ними идут количество поддерживаемых платформ и жанр. Год выпуска, рейтинг и оценка пользователей мало влияют на продажи (продажи зависят от оценки пользователей, но оценка критиков лучше моделирует продажи, а оценка пользователей практически не превносит никакой новой информации).
За все время:
За последние 5 лет:
По регионам:
Население Европы и Северной Америки имеют схожие вкусы. Они играют на современных настольных консолях в шутеры, гонки и платформеры. Предпочитают игры с рейтингом M или E10+. Японцы же играют в портативные консоли (PSP, PSV, 3DS), и играют в основном в ролевые игры, файтинг и стратегии. Для них предпочтительнее игры без рейтинга и игры с рейтингом T.
Гипотезы: